# MediaPlayer - MediaPlayer that plays audio and video streams
# Copyright (C) 2007 Lauri Taimila
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

__licence__ = "GPLv2"
__copyright__ = "2007, Lauri Taimila"
__author__ = "Lauri Taimila <lauri@taimila.com>"

import os
import gst
import gtk
import clutter
from clutter import cluttergst

from frontend.medialibrary.playable import Playable

class MediaPlayer(object):
    """
    MediaPlayer class for Entertainer

    MediaPlayer uses Gstreamer backend and is able to play all video and audio
    files that gstreamer supports. Entertainer has only one MediaPlayer object
    at runtime.
    
    MediaPlayer can play objects that implement Playable interface. These
    classes are Track, Movie, TVEpisode, Movie, VideoClip and CompactDiscTrack. 
    (and Channel in the future)
    """
    
    # Ratio constants
    NATIVE = 0
    WIDESCREEN = 1
    NORMAL = 2
    LETTER_BOX = 3
    ZOOM = 4
    INTELLIGENT = 5
    
    def __init__(self, stage, width, height):
        """
        Initialize media player.
        @param stage: Stage object
        @param width: Stage width (This is not a current size, but initialized)
        @param height: Stage height (This is not a current size, but initialized)
        """
        self.stage = stage               # Stage that displays textures
        self.bgcolor = stage.get_color() # Stage background color when not playing
        self.stage_width = width         # Stage width used for video resizing
        self.stage_height = height       # Stage height used for video resizing
        self.ratio = MediaPlayer.NATIVE  # Video texture ratio
        
        self.audio_skip_step = 10        # Audio skip step in seconds
        self.video_skip_step = 60        # Video skip step in seconds
        self.playlist = None             # Current play list
        self.media = None                # Current media (Playable object)
        self.shuffle = False             # Shuffle mode
        self.repeat = False              # Repeat mode
        self.playing = False             # Is media player currently playing
                
        self.video_texture = cluttergst.VideoTexture()
        self.playbin = self.video_texture.get_playbin()
        self.bus = self.playbin.get_bus()
        self.bus.add_signal_watch()
        self.bus.connect('message', self._on_gst_message)

        self.video_texture.connect("size-change", self.change_ratio_callback)
        self.video_texture.show()
    
    def _on_gst_message(self, bus, message):
        """
        Callback function that is called every time when message occurs on
        Gstreamer messagebus.
        """
        if message.type == gst.MESSAGE_EOS:
            if self.media.get_type() == Playable.VIDEO_STREAM or self.playlist is None:
                self.video_texture.set_playing(False)
                self.video_texture.set_property("position", 0)
            else:
                self.next()
    
    def set_playlist(self, playlist):
        """
        Set new playlist to MediaPlayer. MediaPlayer plays media from this list.
        @param playlist: Playlist object
        """
        if len(playlist) == 0:
            raise exception("Empty playlist is not allowed!")
        self.playlist = playlist
        self.set_media(self.playlist.get_current(), True)
    
    def get_playlist(self):
        """
        Get current playlist
        @return: Playlist object
        """
        return self.playlist
    
    def set_media(self, playable, internal_call = False):
        """
        Set media to media player. Media is an object that implements 
        Playable interface. This media is played back when play() is called.
        @param playable: Playable object
        """
        # If this function is called from this object we don't set playlist to None
        if not internal_call:
            self.playlist = None
        
        # If player is currently playing then we stop it
        if self.is_playing():
            self.stop()
        
        # Update media information
        self.media = playable
        
        # Set up media player for media
        if self.media.get_type() == Playable.AUDIO_STREAM or self.media.get_type() == Playable.VIDEO_STREAM:
            self.video_texture.set_uri(playable.get_uri())
        elif self.media.get_type() == Playable.DVB_STREAM:
            raise Exception("DVB Support not implemented yet!") #FIXME
        
    def get_media(self):
        """
        Get URI of the current media stream.
        @return: String
        """
        return self.media
    
    def has_media(self):
        """
        Has media been set to this player. == has set_media() been called before
        @return: Boolean value. True if media has been set, otherwise False
        """
        if self.media is None:
            return False
        else:
            return True
    
    def get_media_type(self):
        """
        Get the type of the current media
        @return: Integer (See Playable interface constants)
        """
        return self.media.get_type()
    
    def set_shuffle(self, boolean):
        """
        Enable or disable shuffle play. When shuffle is enabled MediaPlayer picks
        a random Playable from the current playlist.
        @param boolean: True to enable / False to disable
        """
        self.shuffle = boolean
    
    def is_shuffle_enabled(self):
        """
        Is shuffle enabled?
        @return: True if enables, otherwise False
        """
        return self.shuffle
    
    def set_repeat(self, boolean):
        """
        Enable or disable repeat mode. When repeat is enabled the current
        playable is repeated forever.
        """
        self.repeat = boolean
        
    def is_repeat_enabled(self):
        """
        Is repeat enabled?
        @return: True if enables, otherwise False
        """
        return self.repeat
    
    def play(self):
        """
        Play current media
        """
        # If current media is an audio file
        if self.media.get_type() == Playable.AUDIO_STREAM:
            self.playing = True
            self.video_texture.set_playing(True)
            
        # If current media is a video file
        elif self.media.get_type() == Playable.VIDEO_STREAM:
            self.stage.add(self.video_texture)
            self.video_texture.lower_bottom()
            self.playing = True
            self.stage.set_color((0,0,0,0))
            self.video_texture.set_playing(True)
        
        # If current media is DVB Digital TV broadcast
        elif self.media.get_type() == Playable.DVB_STREAM:
            pass #FIXME: DVB Support is not implemented yet
        
    def pause(self):
        """
        Pause media player
        """
        self.playing = False
        self.video_texture.set_playing(False)
        
    def stop(self): 
        """
        Stop media player
        """
        self.playing = False
        if self.media.get_type() == Playable.VIDEO_STREAM:
            self.stage.set_color(self.bgcolor)
            self.stage.remove(self.video_texture)
        self.video_texture.set_playing(False)
        self.video_texture.set_property("position", 0)
    
    def next(self):
        """
        Play next track / video from current playlist
        """
        if self.playlist is not None:
            if self.shuffle:
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_next():
                self.set_media(self.playlist.get_next(), True)
            self.play()
        
    def previous(self):
        """
        Play previous track / video from current playlist
        """
        if self.playlist is not None:
            if self.shuffle: #FIXME: Should we remember order of randomly played tracks
                self.set_media(self.playlist.get_random(), True)
            elif self.playlist.has_previous():
                self.set_media(self.playlist.get_previous(), True)
            self.play()
    
    def skip_forward(self):
        """Skip media stream forward"""
        if self.media.get_type() == Playable.VIDEO_STREAM:    
            pass
        elif self.media.get_type() == Playable.AUDIO_STREAM:
            pos_int = self.playbin.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.playbin.query_duration(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int + (self.audio_skip_step * 1000000000)
            if seek_ns > dur:
                seek_ns = dur
            self.playbin.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
            
    def skip_backward(self):
        """Skip media stream backward"""
        if self.media.get_type() == Playable.VIDEO_STREAM:
            pass
        elif self.media.get_type() == Playable.AUDIO_STREAM:
            pos_int = self.playbin.query_position(gst.FORMAT_TIME, None)[0]
            seek_ns = pos_int - (self.audio_skip_step * 1000000000)
            if seek_ns < 0:
                seek_ns = 0
            self.playbin.seek_simple(gst.FORMAT_TIME, gst.SEEK_FLAG_FLUSH, seek_ns)
    
    def is_playing(self):
        """
        Is media player currently playing.
        @return: Boolean
        """
        return self.playing
    
    def get_media_position(self):
        """
        Get current position of the play back
        @return: Float between 0 and 1
        """
        try:
            pos = self.playbin.query_position(gst.FORMAT_TIME, None)[0]
            dur = self.playbin.query_duration(gst.FORMAT_TIME, None)[0]
        except:
            return 0
        dur_sec = dur / 1000000000.0
        pos_sec = pos / 1000000000.0
        return pos_sec / dur_sec
    
    def get_media_position_string(self):
        """
        Get current position of the play back as human readable string
        @return: Human readable time stamp. For example 04:23
        """
        ns = self.playbin.query_position(gst.FORMAT_TIME, None)[0]
        return self._convert_ns_to_human_readable(ns)
    
    def set_media_position(self, position):
        """
        Set position of the current media.
        @param position: New position. Float from 0-1.
        """
        pass #TODO: Implement set_media_position() method in MediaPlayer
    
    def get_media_duration_string(self):
        """
        Return media duration in string format. Example 04:20
        This code is borrowed from gStreamer python tutorial.
        @return: Duration of the media
        """
        ns = self.playbin.query_duration(gst.FORMAT_TIME, None)[0]
        return self._convert_ns_to_human_readable(ns)
    
    def _convert_ns_to_human_readable(self, time_int):
        """
        Convert nano seconds to human readable time string.
        This code is borrowed from gStreamer python tutorial.
        @param time_int: Time in nano seconds
        @return: Human readable time stamp. For example 04:23
        """
        time_int = time_int / 1000000000
        time_str = ""
        if time_int >= 3600:
            _hours = time_int / 3600
            time_int = time_int - (_hours * 3600)
            time_str = str(_hours) + ":"
        if time_int >= 600:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + str(_mins) + ":"
        elif time_int >= 60:
            _mins = time_int / 60
            time_int = time_int - (_mins * 60)
            time_str = time_str + "0" + str(_mins) + ":"
        else:
            time_str = time_str + "00:"
        if time_int > 9:
            time_str = time_str + str(time_int)
        else:
            time_str = time_str + "0" + str(time_int)    
        return time_str
    
    def change_ratio_callback(self, texture, width, height):
        """
        Callback for changing video texture's aspect ratio. This is called when 
        video texture size changes.
        @param texture: Video texture which size has changed
        @param width: Width of the current media
        @param height: Height of the current media
        """
        if self.ratio == MediaPlayer.NATIVE:
            self.set_native_ratio(width, height)
        elif self.ratio == MediaPlayer.WIDESCREEN:
            self.set_widescreen_ratio(width, height)
        elif self.ratio == MediaPlayer.ZOOM:
            self.set_zoom_ratio(width, height)
        elif self.ratio == MediaPlayer.INTELLIGENT:
            self.set_intelligent_ratio(width, height)
            
    def set_native_ratio(self, width=None, height=None):
        """
        Do not stretch video. Use native ratio, but scale video such a way
        that it fits in the window.
        @param width: Width of the current media
        @param height: Height of the current media
        """
        self.ratio = MediaPlayer.NATIVE
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                w, h = self.video_texture.get_size()
            else:
                w = width
                h = height
            x_ratio = self.stage_width / float(w)
            y_ratio = self.stage_height / float(h)
            
            if x_ratio > y_ratio:
                self.video_texture.set_scale(self.stage_height/float(h), 
                                       self.stage_height/float(h))
                new_width = int(w * (self.stage_height/float(h)))
                new_x = int((self.stage_width - new_width)/float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(self.stage_width/float(w), 
                                       self.stage_width/float(w))
                new_height = int(h * (self.stage_width/float(w)))
                new_y = int((self.stage_height - new_height)/float(2))
                self.video_texture.set_position(0, new_y)
                
    def set_widescreen_ratio(self, width=None, height=None):
        """
        Stretch video to 16:9 ratio.
        @param width: Width of the current media
        @param height: Height of the current media
        """
        self.ratio = MediaPlayer.WIDESCREEN
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                w, h = self.video_texture.get_size()
            else:
                w = width
                h = height
            self.video_texture.set_scale(self.stage_width/float(w), 
                                   self.stage_height/float(h))
            self.video_texture.set_position(0,0)
    
    def set_letter_box_ratio(self, width=None, height=None):
        """
        Set video playback into letter box mode.
        @param width: Width of the current media
        @param height: Height of the current media
        """
        self.ratio = MediaPlayer.LETTER_BOX
        raise Exception("set_letter_box_ratio() is NOT implemented!")
        
    def set_zoom_ratio(self, width=None, height=None):
        """
        Stretch video to screen such a way that video covers most of the screen.
        @param width: Width of the current media
        @param height: Height of the current media
        """
        self.ratio = MediaPlayer.ZOOM
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            if width is None and height is None:
                w, h = self.video_texture.get_size()
            else:
                w = width
                h = height
            x_ratio = self.stage_width / float(w)
            y_ratio = self.stage_height / float(h)
            
            if x_ratio < y_ratio:
                self.video_texture.set_scale(self.stage_height/float(h), 
                                       self.stage_height/float(h))
                new_width = int(w * (self.stage_height/float(h)))
                new_x = int((self.stage_width - new_width)/float(2))
                self.video_texture.set_position(new_x, 0)
            else:
                self.video_texture.set_scale(self.stage_width/float(w), 
                                       self.stage_width/float(w))
                new_height = int(h * (self.stage_width/float(w)))
                new_y = int((self.stage_height - new_height)/float(2))
                self.video_texture.set_position(0, new_y)
    
    def set_intelligent_ratio(self, width=None, height=None):
        """
        This aspect ratio tries to display 4:3 on 16:9 in such a way that
        it looks good and still uses the whole screen space. It crops some of 
        the image and does some stretching, but not as much as 
        set_widescreen_ratio() -method.
        @param width: Width of the current media
        @param height: Height of the current media
        """
        self.ratio = MediaPlayer.INTELLIGENT
        if self.has_media() and self.media.get_type() == Playable.VIDEO_STREAM:
            ratio = 1.555555555 # 14:9 Aspect ratio
            if width is None and height is None:
                w, h = self.video_texture.get_size()
            else:
                w = width
                h = height
            fake_height = self.stage_width / ratio # Fake stage aspect ratio
            self.video_texture.set_scale(self.stage_width/float(w), 
                                   fake_height/float(h))
            y_offset = -int((fake_height - self.stage_height) / 2)
            self.video_texture.set_position(0, y_offset)
            
    def get_texture(self):
        """
        Get media's texture. This is a video texture or album art texture.
        @return: clutter.Actor
        """
        if self.media.get_type() == Playable.VIDEO_STREAM:
            return clutter.CloneTexture(self.video_texture)
        
        elif self.media.get_type() == Playable.AUDIO_STREAM:
            url = self.media.get_album_art_url()
            if url is not None:
                texture = clutter.Texture()
                texture.set_pixbuf(gtk.gdk.pixbuf_new_from_file(url))
                return texture
            else:
                pass #FIXME: Return default album art
            
        elif self.media.get_type() == Playable.DVB_STREAM:
            raise Exception("DVB Support is not implemented yet!")
